//
// Created by LiuYou on 2021/5/23.
//

#include <cstdlib>
#include <unistd.h>
#include <cstdio>
#include <sys/wait.h>
#include <cstring>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/syslog.h>
#include <cerrno>
#include <csignal>


static FILE* fp;


static int daemonize();

// TODO 注意: 重要
//  s值是应对 多个信号共用同一个信号处理函数时的情况。
static void daemonExit(int s) {
//    if (s == SIGINT) { ;
//    } else if (s == SIGTERM) { ;
//    } else if (s == SIGQUIT) { ;
//    }


    std::fclose(fp);
    closelog();

    std::exit(0);
}

/*
 * TODO 注意: 现在我设计的这个守护进程有一些缺陷:
 *  如: 有些perror()这些错误都会往标准错误上走。但是此时守护进程已经脱离控制终端了。
 *  守护进程已经不与0 1 2这些设备关联了。
 *  解决改进的方法是系统日志的写法。
 *
 *
 *  TODO 注意: 对之前的 MyDaemon 进行改进, 使其异常终止设计为正常结束。
 */
int main() {
    // TODO 注意: 多个signal()函数会有重入的情况。
    //  所以使用 sigaction()函数来代替它。
//    signal(SIGINT, daemonExit);
//    signal(SIGQUIT, daemonExit);
//    signal(SIGTERM, daemonExit);

    struct sigaction sa{};

    sa.sa_handler = daemonExit;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGINT);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, nullptr);
    sigaction(SIGQUIT, &sa, nullptr);
    sigaction(SIGTERM, &sa, nullptr);


    openlog("MyDaemon", LOG_PID, LOG_DAEMON);

    // 设计daemonize();函数来实现守护进程
    int ret = daemonize();
    if (ret == -1) {
        // 如果出错了, 把当前出错的内容当成系统日志的提交。
        // TODO 注意: 在字符串中不需要写换行符, 因为日志的格式是有syslog来规定的。
        syslog(LOG_ERR, "Daemonize() 失败!");
        std::exit(1);
    } else {
        syslog(LOG_INFO, "Daemonize() 成功!");
    }

    // 守护进程需要做的事情, 设计守护进程的功能。
    const char* fileName = "/tmp/out";
    fp = std::fopen(fileName, "w");
    if (fp == nullptr) {
        // TODO 注意: 这里使用std::perror()函数是不太正确的, 因为守护进程已经推理终端控制了。
        //  不能进行标准输入输出了。
//        std::perror("fopen()");
        // TODO 注意: 对上一句的改进！
        syslog(LOG_ERR, "fopen(): %s", std::strerror(errno));
        std::exit(1);
    }

    syslog(LOG_INFO, "%s 打开了", fileName);

    for (int i = 0;; ++i) {
        // TODO 注意: fprintf()函数是行缓冲模式, 但是文件是全缓冲模式。
        //  所以需要使用fflush()函数去刷新流这个操作。
        std::fprintf(fp, "%d\n", i);
        std::fflush(fp);
        syslog(LOG_DEBUG, "%d 已经打印了", i);
        sleep(1);
    }

    // TODO 注意: 下面的语句是执行不到的。因为还没有学信号等进程控制, 进程的结束只能通过kill来终止该守护进程。
    // TODO 注意: 当前这个程序并不完善, 将来会学习新的机制来解决这个问题。把当前进程的异常终止改为
    //  正常终止。因为当前要想使守护进程终止, 是通过信号kill来杀死它。
//    std::fclose(fp);
//    closelog();


    std::exit(0);
}

static int daemonize() {

    // fork()
    pid_t pid = fork();
    if (pid < 0) {
//        std::perror("fork()");
//        std::exit(1);
        return -1;
    }
    if (pid > 0) {
        // 父进程 parent
        std::exit(0);

    }

    // child
    // 如果是子进程才调用setsid()
    // 而且, 在调用setsid()之前要做一些铺垫性的工作。
    // 比如: 这个子进程变成守护进程后是要脱离控制终端的,
    // 那么0 1 2 这三个文件描述符就没有必要, 或者说是不去关联设备。
    // 所以需要重定向。

    // 打开这个文件是要进行重定向。
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0) {
//        std::perror("open()");
//        std::exit(1);
        return -1;
    }

    // 在进行setsid()之前对0 1 2进行重定向。
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2) {
        close(fd);
    }


    // setsid() 但是要子进程调用这个函数, 所以需要在之前调用fork()。
    // 子进程称为守护进程。父进程正常退出。
    setsid();


    // 对守护进程进行收尾的处理。
    // 如果一个守护进程一直在跑的话, 一般会使用chdir()函数。
    // 把当前的工作路径设置到根路径下, 根目录的位置。
    chdir("/");
    // 还有其它收尾的处理, 如: 比如你确定这个守护进程不会产生文件了,
    // 那就可以将umask值关掉。
    // 当然, 如果守护进程还会产生文件, 那可以不关掉umask, 然后对它限制一下权限就可以了。
//    umask(0);

    // 函数正常退出。
    return 0;
}

